-
-
Notifications
You must be signed in to change notification settings - Fork 156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Imperial Units Feature #365
Conversation
…lations and tests
Looks good :) |
Okay thanks @PierreBresson! |
@PierreBresson could you please take a look at d03df83? It includes the implementation of getImperialMetricValue() in the calculation util as discussed here in #236 as well as application of the useMetricUnits state in the emission-adding components. In implementing the application of useMetricUnits in those components, I discovered a variance in the way they called their respective emission-setting functions passed in through props (e.g. setElectricityConsumption() in the electricity emission-adding component, and setQuantity() in the fashion emission-adding component). To use these two as an example (although the difference was apparent in multiple components), pre-commit the electricity component called setElectricityConsumption() directly in the component (so on re-render, essentially), while the fashion component called setQuantity() in an onSliderValueChange() function passed as its slider's onSlidingComplete prop. Both methods effectively cause the respective methods to be called when the slider changes, but the former way caused a warning to arise when accessing those components in development mode, and the latter way seemed (to me at least) to establish more intention and organization in the code, even if just a little bit. So I standardized that pattern across the components. EDIT Since the streaming component has some extra complexity for determining which values/units to display, I deviated from the patterns established in the other components by extracting some of that logic to immediately called functions in the top section of the component, rather than keeping it in its return statement. So it has some variables/functions, const displayCarbonValue = (() => {
if (carbonValue > 1 && useMetricUnits) {
return carbonValue;
} else if (carbonValue <= 1 && useMetricUnits) {
return carbonValue * 1000;
} else {
return getImperialMetricValue(carbonValue, useMetricUnits, MeasureType.mass);
}
})();
const displayUnits = (() => {
if (carbonValue > 1 && useMetricUnits) {
return "kgCO2eq";
} else if (carbonValue <= 1 && useMetricUnits) {
return "gCO2eq";
} else {
return "lbsCO2eq";
}
})(); in the top section of the component, instead of implementing that logic with nested ternaries in the returned TSX section of the component. I used immediately called functions so that the associated variables could be END EDIT Lastly, I'm not sure how/where to go about testing these changes (and I admittedly haven't given it a whole lot of thought), but it seems to me that I should write some tests to assert that the actual presented values are as expected given either value of the useMetricUnits state. If my suspicions are correct, and tests are indeed in order (are they?), then can I do some brainstorming about how to implement them and then present my plan to you for approval? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works well, just a few changes 👍
<Text.Primary lightGray>{Math.round(sliderValue) + " kilometer(s)"}</Text.Primary> | ||
<Text.Primary lightGray>{ | ||
Math.round(getImperialMetricValue(sliderValue, useMetricUnits, MeasureType.length)) | ||
+ (useMetricUnits ? " kilometer(s)" : " miles(s)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mile(s)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
setCo2eqKilograms(emissionAmount); | ||
const onSliderValueChange = (value: number) => { | ||
setSliderValue(value); | ||
setCo2eqKilograms(emissionAmount);; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
double ;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
app/utils/calculation/calculation.ts
Outdated
@@ -136,11 +136,34 @@ const getPeriodicityText = ({ | |||
return periodicityText; | |||
}; | |||
|
|||
enum MeasureType { mass = "mass", length = "length", } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be moved in app/types
folder (just create something like guides.ts and import it here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made app/types/measureType.ts in the most recent push
return ( | ||
<View style={containerStyle}> | ||
<Text.Secondary numberOfLines={1}>{title}</Text.Secondary> | ||
<Switch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll need some help with this one. I added some padding to the <Switch> descendant component of <ListItemSwitch> in the most recent push to maybe help fix this, but I can't actually perform a visual check since I don't own anything that can run or emulate iOS (as far as I know).
Regarding the tests, there is correctly no test checking the entire add emission flow, so it's fine if you don't add any. I'm planning to change the slider to some input field at some point and I will add more tests then. |
I learned something neat today in researching this: the ton used in America (and some industries in Canada) is different from the ton used in the UK and Commonwealth of Nations. Here, "ton" references the short ton, equivalent to 2,000 lb, but in the UK, "ton" references the long ton, equivalent to 2,240 lb (https://en.wikipedia.org/wiki/Ton). Would you want me to separate the unit preference into 3 choices (Metric, Imperial and US/CAN) to accommodate for this difference? Alternatively, and in my own opinion as a user of Imperial system, I don't think it's such a hindrance to keep using pounds even when the scale might permit converting to tons (although it is a pretty common practice to switch as such, at least in my own country). Either way, I'm happy to implement what's best for the app. I'll implement a conversion to ounces for small scales. Personally, I typically think in terms of fractional pounds, even when presented with a measurement in ounces (e.g. if I see an 8 oz. steak on a menu, I'm thinking, "Oh, like half a pound.") since that's where I have the most context, but that might not be true for all subjects of the Imperial system, and it's not uncommon to see the weight of food products given in ounces. EDIT I also found out that while "lbs" is an acceptable abbreviation for the plural of "pound" for common purposes, technically the proper abbreviation is "lb". I'll go ahead and make the code reflect this change if you'd like. EDIT 2 The 3 options I above referenced as "Metric, Imperial and US/CAN" might be better described as "Metric, Imperial and United State customary units." |
I've got format-on-save configured now in my settings.json. Not sure if I'm using the correct format standard though. I tried one, but it removed all lines between import groups, so I tried another, and it removed spacing in the code while maintaining the separation of imports. |
Okay, I extracted those functions as getDisplayUnitsValue and getDisplayUnits, respectively, so I'll test them too. Do you have any feedback on my implementation and usage of them? EDIT Wrote tests for the functions I added to calculation.ts: getImperialMetricValue, getDisplayUnitsValue and getDisplayUnits |
Didn't quite get to these in the last push, although I certainly intend to. Thanks for the opportunity to contribute. If I'm moving too slowly with this feature, I won't have any hard feelings if you jump in to wrap it up EDIT Implementing units based on user preference for the country list in the Monthly Emissions screen is making me want to provide localization for the unit abbreviations. Also, I'd like to add a boolean argument to the getDisplayUnits() function to return the full name for the unit instead of the symbol (e.g. "kilograms" vs "kg"), with respect to the current locale. I might be kind of reinventing the wheel here with localization of the units. This looks helpful. But I definitely don't need that whole "wheel," just a few spokes, so to speak. Plus it doesn't look like it has support beyond symbols if you're good with me adding support for full unit names in addition to the symbols. |
Okay, for all I wrote in the comments above, I think now I can say I've got everything in a place ready for review. |
} | ||
}; | ||
|
||
const getDisplayUnits = (kgValue: number, useMetricUnits: boolean, useSymbol = true): string => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified / more readable :
const getDisplayUnits = (kgValue: number, useMetricUnits: boolean, useSymbol = true): string => {
const suffix = useSymbol ? "_SYMBOL" : "_FULL";
if (useMetricUnits) {
if (kgValue <= 1) {
return t(`GRAMS${suffix}`);
} else if (kgValue > 1 && kgValue <= 1000) {
return t(`KILOGRAMS${suffix}`);
} else {
return t(`TONNES${suffix}`);
}
} else {
if (kgValue <= 0.454) {
return t(`OUNCES${suffix}`);
} else {
return t(`POUNDS${suffix}`);
}
}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's much more readable. Satisfying too. Thanks for the coaching. I like to code, but I know I write spaghetti haha, so learning patterns like this (extracting booleans common throughout an if-else block to an outer if-else block; not sure if there's a technical name for it) is really cool and one of the reasons I wanted to start contributing to open source. It makes sense (maybe even common sense to others), it just didn't occur to me that doing so would make the code easier to read and understand. Taking advantage of the naming "_SYMBOL/_FULL" convention to simplify all the ternaries to a single ternary is really satisfying too. Would you be able to recommend any resources for learning practices like these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that you usually don't need any comments when the code is "okayish" and the naming of the variables/function is also important, I prefer long and explicit, but that could be arguable sometimes. For e.g, it's usually good practice to have has
/is
prefix for bool
variables and then when you see duplicate code it means you can refactor. I would say it comes with times and looking at code from different large projects (with several contributors) with different way of coding, it helps you to figure out your style, and teach how to write reusable and easy to understand code.
Also, for this scenario, I was also thinking of using functional programming (FP) with ramda.js, but it's not so necessary as it's not a big piece of code. However, when you have to process and transform a lot of data, FP helps you to write concise & reusable code (for e.g have a look to getEmissions
in EmissionsScreen.selectors.ts
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm okay. I have noticed that the long and explicit names in this repo have made it pretty easy to follow without extensive commenting. Duplicate code => you can refactor; of course. I mean feels like that should've been common sense again, and it's something I've sometimes followed naturally, but I'm not sure I've ever codified it in my head.
And sweet, thanks for formally introducing me to FP. Just did some research on it. Looks like another thing I've been kind of using but not with much intention. That flow in the Emissions selector is very readable and concise. I hope I come across some kind of transformation-heavy situation soon to apply it
} | ||
}; | ||
|
||
const getDisplayUnitsValue = (kgValue: number, useMetricUnits: boolean): number => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified / make more readable (see my comment on getDisplayUnits)
maximumFractionDigits={displayValue >= 1 ? 2 : 4} | ||
value={displayValue} | ||
/> | ||
{" " + getDisplayUnits(co2value, useMetricUnits)}CO2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just realized that now : we are missing the eq
for some reason 😅 {" " + getDisplayUnits(co2value, useMetricUnits)}CO2eq
<FormattedNumber | ||
value={Math.round(getDisplayUnitsValue(kgCarbonValue, useMetricUnits))} | ||
/>{" "} | ||
{getDisplayUnits(kgCarbonValue, useMetricUnits, !(kgCarbonValue > 1000 && useMetricUnits))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be simplified, as we only want the short version so it's more readable :
{getDisplayUnits(kgCarbonValue, useMetricUnit)}
{"CO2eq"}
We are also missing the co2eq for some reason
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay. I was trying to maintain the long version for tons to keep the UI the way it was before, but okay yeah I'll make the change so that for all units only the symbol is displayed here. Also oops on the co2eq
@@ -86,10 +105,14 @@ const MonthlyBudgetScreen: NavStatelessComponent = () => { | |||
/> | |||
</Text.Primary> | |||
</View> | |||
{translationMontlyBudgetCountries.map(CountryExample)} | |||
{translationMonthlyBudgetCountries.map((countryArr, idx) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
@@ -40,6 +40,55 @@ exports[`SettingsScreen renders correctly 1`] = ` | |||
size={14} | |||
/> | |||
</TouchableOpacity> | |||
<View |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to have : if you add the following two lines to jest/mock/components/index.js
you will mock globally ListItem
and ListItemSwitch
and this snapshot will become more readable
jest.mock("../../../app/components/ListItem", () => "ListItem");
jest.mock("../../../app/components/ListItemSwitch", () => "ListItemSwitch");
paddingVertical: 18, | ||
flex: 1, | ||
}, | ||
switch: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
switch style can be deleted. Instead, remove paddingVertical: 18,
from the container and create a text style like this :
text {
paddingVertical: 18,
},
And then :
<Text.Secondary style={styles.text} numberOfLines={1}>
{title}
</Text.Secondary>`
This should fix your alignments issues ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah that worked. Comparing ListItem and its styles to ListItemSwitch and its styles now to try and figure out why it worked. Could we make the same change to ListItem (move the padding to the container component's <Text> child) to achieve the same result? And moreover, why does moving the padding inward help center the switch? Is it because the container style's alignItems: center
, and when you add padding to the container it changes the center slightly (that doesn't make a whole lot of sense, but I'm not sure what's going on)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the components inside a parent component are almost the same size, then applying the padding to the parent is usually the best way to go. However, when you have different size component inside one parent which has the padding, then you can have some kind of "overflow" like in ListItemSwitch. In my experience, the best way to visualize/debug that is to put different background color to the different components, it helps understanding how to apply constraints in a "lego/tetrix" way. So yeap, I think it's thanks to the alignItems: center
that child components are aligned now (I did a quick debugging yesterday so not sure 100%)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh okay, that makes sense. I'm guessing then you want to apply the padding to the component with the smallest size in the target axis? I should probably review the box model. Also changing the colors totally helped visualize that and enabled me to actually play around with it meaningfully instead of pure guess-and-check. Thanks!
@@ -76,9 +76,10 @@ exports[`EmissionListItem renders correctly if mitigated 1`] = ` | |||
> | |||
<FormattedNumber | |||
maximumFractionDigits={2} | |||
value={2.1} | |||
value={4.6305000000000005} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to keep the kg values in snapshots across the entire project, or create two snapshots files like EmissionListItem.metric.test.tsx.snap
and EmissionListItem.imperial.test.tsx.snap thanks to EmissionListItem.metric.test.tsx
and EmissionListItem.imperial.test.tsx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put together a perhaps janky way to get this working, using the getUseMetricUnits selector to mock useSelector before each test in the affected snapshots. EmissionItemScreen I only mocked useSelector in this once per test since it uses other selectors, and this also required me to move the related line of code above all the other useSelector calls in order for it to receive the intended mocking
EDIT
I apologize for my unclear language here -- it's like 2 am, and I can't put my words together right now
I'm good with not display imperial tons, as I think it would introduce too much complexity, and this current implementation is good enough to be released. |
Yeah, I think too we should keep as it is now or it would be re-inventing the wheel. It would be nice to find a library like the .net for js so we could have the rendering of the units based on the phone settings, like the language, or through a list of option. |
Correct, I've played with the app and everything seems to be working/implemented in all the screens, well done 👏 Just little adjustments we can merge :) |
Hey @PierreBresson is this by any chance ready to merge? |
@ThomasLatham It's merged! 🥳 Let me know if you want some stickers by email, I will send you some ;) I'm going to make a release soon with this feature. |
@PierreBresson Thanks so much! That would be awesome to get some stickers :D This was basically my first moderate-size contribution to an open-source project (everything before was basically one or two lines). I learned a ton and am really grateful for the opportunity! |
✅ I have read the contributing file
Summary
The purpose of this PR is to introduce functionality for a switch-row in settings that controls some application state for unit preferences, as part of #236. It currently does not include implementation of anything that would apply that state where applicable. Specifically, this PR:
creates a <ListItemSwitch> component (and related tests) that's essentially <ListItem> but with a Switch instead of a chevron. Also, instead of the onPress prop, it has onChange and value props.
updates the userPreferences slice and selectors (and corresponding tests) to include a useMetricUnits boolean in the Redux store with a default value of true. The toggleUnits reducer, like toggleNotification, receives the desired units setting as a boolean payload in an action, rather than just receiving state and setting useMetricUnits to the opposite of its current value.
creates a new row in the Settings screen under About, with the English title "Metric units" (the translations were updated for all languages to reflect this, though). It is a <ListItemSwitch>, receiving useMetricUnits's selector as its value prop and, for its onChange prop, a dispatch() of the toggleUnits action creator's returned action with the opposite of the value prop as its payload. The Settings snapshot was updated to reflect this change.
Note: Even though #236 indicates that the switch might literally display with the text "Imperial units," I assumed more users would recognize the term "Metric units" and understand that toggling it off would switch the app to that set of units used by "english citizen[s] or ex british colony citizen[s]" (i.e. the imperial system). Further, I omitted the word "use" (in the sense of "Use metric units") for simplicity in translation.
Demo